/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */

import { Flags } from "@oclif/core";
import { type ColumnUserConfig, table } from "table";

import type { Package } from "@fluidframework/build-tools";
import * as changeCase from "change-case";
import { releaseGroupFlag } from "../flags.js";
// eslint-disable-next-line import-x/no-deprecated
import { BaseCommand, isMonoRepoKind } from "../library/index.js";

/**
 * Each displayed column in the report is generated by a corresponding
 * ColumnInfo instance.
 */
interface ColumnInfo<TValue> {
	/**
	 * Function to extract column value from a Package instance.
	 */
	getValue: (pkg: Package) => TValue;

	/**
	 * Format the returned value of 'getValue()' as a string for display in the table.
	 * (Not used for JSON output.)
	 */
	formatValue?: (value: TValue) => string;

	/**
	 * Specifies the appearance of the column.
	 */
	style: ColumnUserConfig;
}

/**
 * Table that maps column names to their corresponding ColumnInfo.
 * Create a new entry in this table to add a new column to the report.
 */
const nameToColumnInfo: Record<string, ColumnInfo<unknown>> = {
	releaseGroup: {
		getValue: (pkg: Package) => pkg.monoRepo?.kind ?? "n/a",
		style: { alignment: "left" },
	},
	name: { getValue: (pkg: Package) => pkg.name, style: { alignment: "left" } },
	private: {
		getValue: (pkg: Package) => pkg.packageJson.private === true,
		formatValue: (value) => (value === true ? "-private-" : ""),
		style: { alignment: "center" },
	},
	version: {
		getValue: (pkg: Package) => (pkg.monoRepo ? pkg.monoRepo.version : pkg.version),
		style: { alignment: "left" },
	},
	path: {
		getValue: (pkg: Package) => pkg.directory,
		style: { alignment: "left" },
	},
};

/**
 * The root `info` command.
 */
export default class InfoCommand extends BaseCommand<typeof InfoCommand> {
	static readonly description = "Get info about the repo, release groups, and packages.";

	static readonly flags = {
		releaseGroup: releaseGroupFlag({
			required: false,
		}),
		columns: Flags.option({
			char: "c",
			description: "Specify which columns are included in report.",
			// Extract the list of valid options from the keys of 'nameToColumnInfo'.
			options: Object.keys(nameToColumnInfo),
			// Include all columns by default except for "path".  (Including "path" tends
			// to make the table extra wide.)
			default: Object.keys(nameToColumnInfo).filter((name) => name !== "path"),
			delimiter: ",",
			multiple: true,
		})(),
		private: Flags.boolean({
			allowNo: true,
			char: "p",
			default: true,
			description: "Include private packages (default true).",
			required: false,
		}),
		...BaseCommand.flags,
	};

	static readonly enableJsonFlag: boolean = true;

	async run(): Promise<Record<string, unknown>[]> {
		const { flags } = this;
		const context = await this.getContext();
		let packages =
			// eslint-disable-next-line import-x/no-deprecated
			flags.releaseGroup !== undefined && isMonoRepoKind(flags.releaseGroup)
				? context.packagesInReleaseGroup(flags.releaseGroup)
				: [...context.fullPackageMap.values()];

		// Sort by packages by name (invariant sort by codepoints).
		// (See https://stackoverflow.com/a/40355107)
		packages.sort(
			(left, right) => Number(left.name > right.name) || -(left.name < right.name),
		);

		// Filter out private packages
		if (!flags.private) {
			packages = packages.filter((p) => p.packageJson.private !== true);
		}

		const columnNames = flags.columns;
		const columnInfos = columnNames.map((name) => nameToColumnInfo[name]);

		// Initialize 'tableData' with column names.
		const tableData = [columnNames.map((name) => changeCase.pascalCase(name))];

		// For the JSON output, we build an array of objects, where the column
		// names are the keys of each object (repeated in each row).
		const jsonData: Record<string, unknown>[] = [];

		for (const pkg of packages) {
			// Create a row for the current package.
			const tableRow: string[] = [];
			const jsonRow: Record<string, unknown> = {};

			// Copy the corresponding column data into the row.
			for (const [index, { getValue, formatValue }] of columnInfos.entries()) {
				const columnName = columnNames[index];
				const value = getValue(pkg);

				// Save the column name/value as a key/value pair in the JSON row.
				jsonRow[columnName] = value;

				// Append to the current table row.  If a 'formatValue' function is
				// specified for the column, apply it to the value before appending.
				tableRow.push(formatValue ? formatValue(value) : String(value));
			}

			// Append the row to the data.
			tableData.push(tableRow);
			jsonData.push(jsonRow);
		}

		const output = table(tableData, {
			columns: columnInfos.map((column) => column.style),
			singleLine: true,
		});

		this.log(`\n${output}`);
		this.log(`Total package count: ${packages.length}`);
		return jsonData;
	}
}
